package yieldCurve;

import java.util.ArrayList;
import java.io.*;

/** YieldCurve reads in spec data and calculates a yield curve */
public class YieldCurve {
	InstrumentData instrumentsData;
	Date today;
	int f; // frequency
	ArrayList<Point> yieldCurve = new ArrayList<Point>(); 
	int right = 0; // left neighbor for interpolation
	int left = 0; // right neighbor for interpolation
	
	/** Constructor
	 * 
	 * @param datafile Filename of yield curve data
	 * @param specfile Filename of yield curve specification
	 * @param frequency Compounding frequency
	 * @param startDate Start date of curve
	 */
	public YieldCurve(String datafile, String specfile, int frequency, Date startDate) {
		// Read in curve data and spec from their CSV files
		instrumentsData = new InstrumentData(datafile, specfile);
		f = frequency;
		today = startDate;
		today = Date.businessDay(today);
		
		run();
	}
	
	/** Perform our calculations
	 * 
	 */
	public void run() {
		// Calculate Cash component of yield curve
		calc_cash();
		
		// Calculate FRA component of yield curve
		calc_fra();
		
		// Calculate Swap component of yield curve
		calc_swap();
		
		// By now yieldcurve should contain a set of date-value pairs representing points of the curve
	}

	// Separate classes to make testing easier
	// Calculate cash component of yield curve
	private void calc_cash() {
		ArrayList<Cash> cash = instrumentsData.getCashList();
		Date date;
		double deltaT;
		double r; // market rate
		double df; // discount factor
		double z; // zero coupon rate
		
		// assert 1M, 2M, 3M must exist
		boolean exist1M = false;
		boolean exist2M = false;
		boolean exist3M = false;
		for (int i = 0; i < cash.size(); i++) {
			if (cash.get(i).subType.equalsIgnoreCase("1M")) {
				exist1M = true;
			} else if (cash.get(i).subType.equalsIgnoreCase("2M")) {
				exist2M = true;
			} else if (cash.get(i).subType.equalsIgnoreCase("3M")) {
				exist3M = true;
			}
		}
		if (!(exist1M && exist2M && exist3M)) {
			throw new Error("At least one of 1M, 2M or 3M Cash record in CSV file is missing.");
		}
		for (int i = 0; i < cash.size(); i++) {
			date = today.getToDate_day(cash.get(i).toDay);
			date = Date.getToDate_month(date, cash.get(i).toMonth);
			deltaT = today.getDeltaT_day(cash.get(i).toDay);
			deltaT += today.getDeltaT_month(cash.get(i).toMonth);
			r = cash.get(i).marketRate;
			df = 1 / (1 + r * deltaT);
			z = dfToZ(df, deltaT);
			yieldCurve.add(new Point(date, deltaT, df, z));
		}
	}
	// Calculate FRA component of yield curve
	private void calc_fra() {
		ArrayList<FRA> fras = instrumentsData.getFRAList();
		double t0; // fromDate - today
		double tToDate; // toDate - today
		double deltaT; // toDate - fromDate
		Date date; // date
		double r; // market rate
		double df; //df at toDate
		double z; // zero coupon rate at toDate
		double zFrom; // zero coupon rate at fromDate
		double dfStart; // df at fromDate
		
		for (int i = 0; i < fras.size(); i++) {
			r = fras.get(i).marketRate;
			t0 = today.getDeltaT_month(fras.get(i).fromMonth);
			tToDate = today.getDeltaT_month(fras.get(i).toMonth);
			date = today.getToDate_month(fras.get(i).toMonth);
			deltaT = tToDate - t0;
			right = getRightNeighbor(t0);
			left = right - 1;
			checkNeighbors();
			// find z at fromDate by interpolation
			zFrom = interpolate(yieldCurve.get(left).time, yieldCurve.get(left).z,
					yieldCurve.get(right).time, yieldCurve.get(right).z, t0);
			dfStart = zToDf(zFrom, t0); // df at from date
			df = dfStart / (1 + r * deltaT);
			z = dfToZ(df, tToDate);
			yieldCurve.add(new Point(date, tToDate, df, z));
		}
		
	}
	
	/**
	 * Find the right neighbor for interpolation
	 * @param deltaT
	 * @return right neighbor's index of keys[] 
	 */
	private int getRightNeighbor(double deltaT) {
		int i;
		int size = yieldCurve.size();

		if (deltaT < yieldCurve.get(0).time) {
			// if deltaT falls on the leftmost of the curve
			right = 1;
		} else if (deltaT >= yieldCurve.get(size - 1).time) {
			// if delta falls on the rightmost of the curve
			right = size - 1;
		} else {
			for (i = 0; i < size - 1; i++) {
				// Find the left and right neighbor of deltaT
				if (yieldCurve.get(i).time <= deltaT && yieldCurve.get(i + 1).time > deltaT) {
					right = i + 1;
					break;
				}
			}
		}

		return right;
	}
	
	private void checkNeighbors() {
		while (yieldCurve.get(right).time == yieldCurve.get(left).time) {
			if (right < yieldCurve.size() - 1) {
				// right is not the last point, move right rightward
				right++;
			} else {
				// right is the last point, move left leftward
				left--;
			}
		}
	}
	
	/** Doing bootstrapping 
	 * @param a list of given swaps
	 * @return a list new bootstrapped swaps
	 */
	private ArrayList<Swap> bootstrap(ArrayList<Swap> swaps) {
		ArrayList<Swap> newSwaps = new ArrayList<Swap>();
		double numOfInterval;
		double current = swaps.get(0).toMonth;
		double next;
		double currentRate = swaps.get(0).marketRate;
		double nextRate;
		double deltaTFrom = today.getDeltaT_month((int)current);
		double deltaTTo;
		double marketRate;
		double to;
		
		for (int i = 0; i < swaps.size() - 1; i++) {
			// Add the known swap i to new bootstrapped swap list
			newSwaps.add(swaps.get(i));
			next = swaps.get(i + 1).toMonth;
			deltaTTo = today.getDeltaT_month((int)next);
			nextRate = swaps.get(i + 1).marketRate;
			// Set default bootstrap interval to 3 months
			numOfInterval = (next - current) / 3;
			// bootstrap from swap i to swap i + 1
			for (int j = 1; j < numOfInterval; j++) {
				// Not real swap, so subtype is set null
				to = current + 3 * j;
				marketRate = interpolate(deltaTFrom, currentRate, deltaTTo, nextRate, today.getDeltaT_month((int)to));
				newSwaps.add(new Swap(Double.toString(to / 12) + "Y", marketRate));
			}
			current = next;
			currentRate = nextRate;
			deltaTFrom = deltaTTo;
		}
		newSwaps.add(swaps.get(swaps.size() - 1)); // add the last swap
		return newSwaps;
	}
	
	/** Calculate swap component of yield curve
	 * 
	 */
	private void calc_swap() {
		double sum = 0; // sum of deltaT * df at time i
		double fromDate = 0; // fromDate - today
		double toDate = 0; // toDate - today
		Date date; // date at toDate
		double deltaT = 0; // toDate - fromDate
		double z; // z at toDate
		double df; // df at toDate
		double marketRate;
		
		// bootstrap from 1st swap onward to the last swap
		ArrayList<Swap> bootstrappedSwap = bootstrap(instrumentsData.getSwapList());
		
		// bootstrap from 1st swap backward to today, work out sum of df*dt
		// for the preparation of swap
		// default bootstrapping step set to 3 months
		for (int i = 0; i < bootstrappedSwap.get(0).toMonth / 3 - 1; i++) {
			toDate = today.getDeltaT_month(3 * (i + 1));
			deltaT = toDate - fromDate;
			z = getZeroCoupon(toDate);
			df = zToDf(z, toDate);
			sum += deltaT * df;
			fromDate = toDate;
		}
		
		for (int i = 0; i < bootstrappedSwap.size(); i++) {
			toDate = today.getDeltaT_month((int)bootstrappedSwap.get(i).toMonth);
			date = today.getToDate_month((int)bootstrappedSwap.get(i).toMonth);
			deltaT = toDate - fromDate;
			marketRate = bootstrappedSwap.get(i).marketRate;
			df = (1 - marketRate * sum) / (1 + marketRate * deltaT);
			sum += df * deltaT;
			z = dfToZ(df, toDate);
			yieldCurve.add(new Point(date, toDate, df, z));
			fromDate = toDate;
		}
	}
	
	/** get value of zero coupon based on discountFactor and delta_t
	 * 
	 * @param discountFactor
	 * @param deltat
	 * @return zero coupon Z
	 */
	private double dfToZ(double df, double deltaT) {
		return (1 / Math.pow(df, (1 / (f * deltaT))) - 1) * f;
	}
	
	/** get df from z
	 * 
	 * @param ZeroCoupon
	 * @param deltat
	 * @return discount factor df
	 */
	private double zToDf(double z, double deltaT) {
		return 1 / Math.pow(1 + z / f, f * deltaT);
	}

	/** Returns the interpolated zero coupon value for the date given in the parameter
	 * 
	 * @param givenDate
	 * @return Zero coupon value for that date
	 */
	public double getZeroCoupon(Date givenDate) {
		double deltaT = Date.getDeltaT(today, givenDate);
		right = getRightNeighbor(deltaT);
		left = right - 1;
		checkNeighbors();
		return interpolate(yieldCurve.get(left).time, yieldCurve.get(left).z,
				yieldCurve.get(right).time, yieldCurve.get(right).z, deltaT);
	}
	
	/** Returns the interpolated zero coupon value for the deltaT given in the parameter
	 * @param deltaT to today
	 * @return zero coupon rate for that delta T
	 */
	public double getZeroCoupon(double deltaT) {
		right = getRightNeighbor(deltaT);
		left = right - 1;
		checkNeighbors();
		return interpolate(yieldCurve.get(left).time, yieldCurve.get(left).z,
				yieldCurve.get(right).time, yieldCurve.get(right).z, deltaT);
	}

	/** get continuous compound rate for a specific date
	 * 
	 * @param givenDate
	 * @return continuous compound rate
	 */
	public double getContinuousCompound(Date givenDate) {
		double deltaT = Date.getDeltaT(today, givenDate);
		double z = getZeroCoupon(givenDate);
		double df = zToDf(z, deltaT);
		return -Math.log(df) / deltaT;
	}
	
	/** get continuous compound rate for a specific date
	 * 
	 * @param deltaT
	 * @return continuous compound rate
	 */
	public double getContinuousCompound(double deltaT) {
		double z = getZeroCoupon(deltaT);
		double df = zToDf(z, deltaT);
		return -Math.log(df) / deltaT;
	}
	
	/** Get discount factor
	 * 
	 * @param givenDate Get d.f. for this date
	 * @return discount factor
	 */
	public double getDf(Date givenDate) {
		double deltaT = Date.getDeltaT(today, givenDate);
		double z = getZeroCoupon(givenDate);
		double df = zToDf(z, deltaT);
		return df;		
	}
	
	public double getDf(double deltaT) {
		double z = getZeroCoupon(deltaT);
		double df = zToDf(z, deltaT);
		return df;
	}
	
	/** linear interpolation between two sets of values
	 *  
	 * @param x1
	 * @param y1
	 * @param x2
	 * @param y2
	 * @param x the value to get the y of.
	 * @return interpolated value of y
	 */
	static public double interpolate(final double x1, final double y1, final double x2, final double y2, final double x) {
		return( (y2-y1) / (x2-x1 ) * (x-x1) + y1);
	}
	
	public void setCompoundingFrequency(int f) {
		this.f = f;
	}
	
	int getCompoundingFrequency() {
		return f;
	}
	
	/** for testing purposes
	 * 
	 */
	public String toString() {
		String s = "dfDate,df\n";
		for (int i = 0; i < yieldCurve.size(); i++) {
			s += yieldCurve.get(i) + "\n";
		}
		return s;
	}
	
	/** Unit test for YieldCurve class
	 * 
	 * @param args no parameters necessary
	 */
	public static void main(String[] args) {
		Date today = new Date(2009, 5, 1);
		Date expiry = new Date(2011, 1, 22);
		YieldCurve yieldCurve = new YieldCurve("curveDataInput.csv", "curveSpec.csv", 2, today);
		double z = yieldCurve.getZeroCoupon(expiry);
		System.out.println("2011,1,22 z=" + z);
		System.out.println(yieldCurve);
		System.out.println(yieldCurve.getContinuousCompound(expiry));
		
		// print yield curve into a csv file
		PrintWriter pc;
		try {
			pc = new PrintWriter(new BufferedWriter(new FileWriter("yieldcurve.csv")));
			pc.println(yieldCurve);
			
			pc.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
			
	}
	
}